# gozerbot/plugins.py
#
#

""" provide plugin infrastructure """

__status__ = "seen"

## gozerbot imports

from gozerbot.stats import stats
from gozerbot.tests import tests
from gozerbot.datadir import datadir
from users import users
from irc.monitor import outmonitor, saymonitor
from xmpp.monitor import xmppmonitor
from utils.log import rlog
from utils.exception import handle_exception
from utils.generic import checkchan
from utils.locking import lockdec, funclocked, Locked
from utils.generic import plugnames, waitforqueue, uniqlist, makeoptions, makeargrest, cleanpycfile
from gozerimport import gozer_import, force_import
from persist.persist import Persist
from persist.persistconfig import PersistConfig
from config import config
from commands import cmnds
from callbacks import callbacks, jcallbacks, gn_callbacks
from redispatcher import rebefore, reafter
from aliases import aliascheck, aliasget
from ignore import shouldignore
from threads.thr import start_new_thread, getname
from persist.persiststate import PersistState
from simplejson import loads
from morphs import inputmorphs, outputmorphs
from eventbase import EventBase
from admin import cmndtable, pluginlist

## basic imports

import os
import os.path
import thread
import time
import Queue
import re
import copy

## locks

loadlock = thread.allocate_lock()
loadlocked = lockdec(loadlock)

## Plugins class

class Plugins(object):

    """ hold all the plugins. """

    def __init__(self):
        self.plugs = {} # dict with the plugins
        self.reloadlock = thread.allocate_lock()
        self.plugdeny = Persist(datadir + os.sep + 'plugdeny', init=False)
        if not self.plugdeny.data: self.plugdeny.data = []
        self.plugallow = Persist(datadir + os.sep + 'plugallow', init=False)
        if not self.plugallow.data: self.plugallow.data = []
        self.avail = [] # available plugins
        self.ondisk = [] # plugisn available for reload
        self.initcalled = [] # plugins which init functions are called
        self.overloads = {} # plugins to overload
        self.activated = {}
        for plug in config['plugdeny']: self.disable(plug)

    def __getitem__(self, item):
        """ return plugin. """
        if self.plugs.has_key(item): return self.plugs[item]
        else: return None

    def get(self, item, attr):
        """ get attribute of plugin. """
        if self.plugs.has_key(item): return getattr(self.plugs[item], attr)

    def whatperms(self):
        """ return what permissions are possible. """
        result = []
        for i in rebefore.whatperms():
            if not i in result: result.append(i)
        for i in cmnds.whatperms():
            if not i in result: result.append(i)
        for i in reafter.whatperms():
            if not i in result: result.append(i)
        result.sort()
        return result

    def exist(self, name):
        """ see if plugin exists. """
        if self.plugs.has_key(name): return True
        return False

    def disable(self, name):
        """
            prevent plugin to be loaded. plugins does get imported but 
            commands, callbacks, monitors etc are not enabled.

        """
        try:
            config['loadlist'].remove(name)
            config.save()
            self.plugdeny.data.append(name)
            self.plugdeny.save()
        except: pass

    def enable(self, name):
        """ enable plugin. """
        try:
            if name not in config['loadlist']:
                config['loadlist'].append(name)
                config.save()
        except KeyError: pass
        try:
            self.plugdeny.data.remove(name)
            self.plugdeny.save()
        except ValueError: pass

    def plugsizes(self):
        """ call the size() function in all plugins. """
        reslist = []
        cp = dict(self.plugs)
        for i, j in cp.iteritems():
            try: reslist.append("%s: %s" % (i, j.size()))
            except AttributeError: pass
        return reslist

    def list(self):
        """ list of registered plugins. """
        self.avail.sort()
        return self.avail

    def plugimport(self, mod, name):
        """ import a plugin. """
        if config.has_key('loadlist') and not name in config['loadlist']: logging.warn("%s is not in loadlist" % name) ; return
        return self.load(mod, name)

    def regplugin(self, mod, name):
        """ register plugin. """
        name = name.lower()
        mod = mod.lower()
        modname = mod + '.' + name
        if name in self.avail: rlog(0, 'plugins', '%s already registered' % name) ; return
        if name in config['plugdeny']: rlog(0, 'plugins', '%s is in config.plugdeny .. not loading' % name) ; return
        if name in self.plugdeny.data: rlog(0, 'plugins', '%s.%s in deny .. not loading' % (mod, name)) ; return 0
        if config.has_key('loadlist') and name not in config['loadlist'] and 'gplugs' in modname and name not in self.plugallow.data:
            rlog(9, 'plugins', 'not loading %s.%s' % (mod, name))
            return 0
        if self.plugs.has_key(name):
            rlog(10, 'plugins', 'overloading %s plugin with %s version' % (name, mod))
            self.unloadnosave(name)
        if not os.path.isdir(datadir + os.sep + 'plugs'):
            os.mkdir(datadir + os.sep + 'plugs')
        if not os.path.isdir(datadir + os.sep + 'plugs' + os.sep + name):
            os.mkdir(datadir + os.sep + 'plugs' + os.sep + name)
        plug = self.plugimport(mod, name)
        if plug:
            rlog(0, 'plugins', "%s.%s registered" % (mod, name))
            if name not in self.avail: self.avail.append(name)
            return plug
        else: rlog(10, 'plugins', "can't import %s.%s .. try plug-enable" % (mod, name))
         
    def showregistered(self):
        """ show registered plugins. """
        self.avail.sort()
        rlog(10, 'plugins', 'registered %s' % ' .. '.join(self.avail))
        self.overload()

    def regdir(self, dirname, exclude=[]):
        """ register a directory. """
        threads = []
        plugs = []
        for plug in plugnames(dirname):
            if plug in exclude or plug.startswith('.'): continue
            try:
                self.regplugin(dirname, plug)
                plugs.append(plug)
            except: handle_exception()
        self.ondisk.extend(plugs)
        return plugs

    def regcore(self): 
        """ register core plugins. """
        self.plugdeny.init([])
        self.plugallow.init([])
        avail = [] 
        plugs = force_import('gozerbot.plugs')
        for i in plugs.__plugs__:
            if i not in avail:
                try: self.regplugin('gozerbot.plugs', i)
                except Exception, ex: handle_exception()
                else: avail.append(i)
        self.ondisk.extend(avail)

    def enableall(self):
        """ enable all plugins. """
        for name in self.available(): self.enable(name)

    def regplugins(self):
        """ register all plugins. """
        self.regcore()
        avail = []
        avail.extend(self.regdir('myplugs'))  
        if os.path.isdir("myplugs"):
            for i in os.listdir('myplugs'):
                if i.startswith('.'): continue
                if os.path.isdir('myplugs' + os.sep + i): avail.extend(self.regdir('myplugs' + os.sep + i))  
        else: rlog(10, 'plugins', 'no myplugs directory found')
        try: gplugs = gozer_import('gplugs')
        except ImportError: rlog(20, 'plugins', "no gplugs package found") ; gplugs = None
        if gplugs:
            for i in gplugs.__plugs__:
                try:
                    self.regplugin('gplugs', i)
                    avail.append(i)
                except Exception, ex: handle_exception()
        if config.get('db_driver') == "olddb":
            try: gplugs = gozer_import('gplugs.olddb')
            except ImportError: rlog(20, 'plugins', "no gplugs.old package found") ; gplugs = None
            if gplugs:
                for i in gplugs.__plugs__:
                    try:
                        self.regplugin('gplugs.olddb', i)
                        avail.append(i)
                    except Exception, ex: handle_exception()
        else:
            try: gplugs = gozer_import('gplugs.alchemy')
            except ImportError: rlog(20, 'plugins', "no gplugs.alchemy package found") ; gplugs = None
            if gplugs:
                for i in gplugs.__plugs__:
                    try:
                        self.regplugin('gplugs.alchemy', i)
                        avail.append(i)
                    except Exception, ex: handle_exception()
        self.ondisk.extend(avail)
        self.readoverload()
        start_new_thread(self.showregistered, ())

    def readoverload(self):
        """ 
            see if there is a permoverload file and if so use it to overload
            permissions based on function name.

        """
        try: overloadfile = open(datadir + os.sep + 'permoverload', 'r')
        except IOError: return
        try:
            for i in overloadfile:
                i = i.strip()
                splitted = i.split(',')
                try:
                    funcname = splitted[0].strip()
                    perms = []
                    for j in splitted[1:]: perms.append(j.strip())
                except IndexError:
                    rlog(10, 'plugins', "permoverload: can't set perms of %s" % i)
                    continue
                if not funcname:
                    rlog(10, 'plugins', "permoverload: no function provided")
                    continue
                if not perms:
                    rlog(10, 'plugins', "permoverload: no permissions provided for %s" % funcname)
                    continue
                self.overloads[funcname] = perms
        except Exception, ex: handle_exception()

    def overload(self):
        """ overload functions in self.overloads. """ 
        for funcname, perms in self.overloads.iteritems():
            if self.permoverload(funcname, perms): rlog(0, 'plugins', '%s permission set to %s' % (funcname, perms))

    def available(self):
        """ available plugins not yet registered. """
        self.ondisk.sort()
        return self.ondisk

    def saveplug(self, plugname):
        """ call save() function of plugin. """
        try: self.plugs[plugname].save()
        except AttributeError: pass
        except KeyError: pass

    def save(self):
        """ call registered plugins save. """
        for plug in self.plugs.values():
            try: plug.save()
            except AttributeError: pass
            except Exception, ex: handle_exception()

    def save_cfg(self):
        """ call registered plugins configuration save. """
        for plug in self.plugs.values():
            try:
                cfg = getattr(plug, 'cfg')
                if isinstance(cfg, PersistConfig):
                    try: cfg.save()
                    except: handle_exception()
            except AttributeError: continue

    def save_cfgname(self, name):
        """ save plugin persisted config data. """
        try:
            plug = self.plugs[name]
            cfg = getattr(plug, 'cfg')
            if isinstance(cfg, PersistConfig):
                try: cfg.save()
                except: handle_exception()
        except (AttributeError, KeyError): pass
	
    def exit(self):
        """ call shutdown on all registered plugins. """ 
        self.save()
        threadlist = []
        for name, plug in self.plugs.iteritems():
            try:
                shutdown = getattr(plug, 'shutdown')
                thread = start_new_thread(shutdown, ())
                threadlist.append((name, thread))
                try: self.initcalled.remove(name)
                except ValueError: pass
            except AttributeError: continue
            except Exception, ex: rlog(10, 'plugins', 'error shutting down %s: %s' % (name, str(ex)))
        try:
            for name, thread in threadlist: thread.join() ; rlog(10, 'plugins', '%s shutdown finished' % name)
        except: handle_exception()

    def getoptions(self, command):
        """ return options entry of a command. """
        return cmnds.getoptions(command)

    def getdepend(self, plugname):
        """ 
            get plugins the plugin depends on.  NOT USED ANYMORE ..

            we use the __depending__ attribute now which checks for the 
            reverse case ala what plugins depends on this plugin. code is 
            in reload().

        """
        if plugname in self.plugs: plug = self.plugs[plugname]
        else:
            for mod in ['gozerbot.plugs', 'gplugs', 'myplugs']:
                try: plug = gozer_import('%s.%s' % (mod, plugname))
                except ImportError: continue
        try: depends = plug.__depend__
        except: depends = []
        return depends

    def load(self, mod , name, enable=True):
        if name in config['plugdeny']: rlog(10, 'plugins', '%s is in deny' % name) ; return 
        modname = mod + '.' + name
        self.down(name)
        self.unload(name)
        if enable: self.enable(name)
        plug = self.plugs[name] = gozer_import(modname)
        plug.loadtime = time.time()
        if enable:
            self.enable(name)
            self.overload()
            try:
                rlog(0, 'plugins', 'calling %s init()' % modname)
                plug.init()
                self.initcalled.append(modname)
            except (AttributeError, KeyError): pass
            except Exception, ex: rlog(10, 'plugins', '%s module init failed' % name) ; raise
            rlog(0, 'plugins', 'enabled %s' % name)
        self.activate(name)
        return plug

    def reload(self, mod, name, enable=True):
        """ reload plugin. """
        if not os.path.isdir(datadir + os.sep + 'plugs'): os.mkdir(datadir + os.sep + 'plugs')
        if not os.path.isdir(datadir + os.sep + 'plugs' + os.sep + name): os.mkdir(datadir + os.sep + 'plugs' + os.sep + name)
        reloaded = []
        modname = mod + '.' + name
        plug = self.load(mod, name)
        try:
            for p in plug.__plugs__:
                self.load(modname, p)
                reloaded.append(p)
        except (KeyError, AttributeError): pass
        rlog(0, 'plugins', 'reloaded plugin %s' % modname)
        reloaded.append(name)
        self.plugallow.data.append(name)
        try: self.plugdeny.data.remove(name)
        except ValueError: pass
        if name not in self.avail: self.avail.append(name)
        try:
            depends = plug.__depending__
            for plug in depends:
                rlog(10, 'plugins', 'loading depending plugin %s (%s)' % (plug, name))
                self.load(mod, plug, False)
                reloaded.append(plug)
        except AttributeError: pass
        return reloaded

    def activate(self, plugname):
        """ activate a plugin. """
        self.activated[plugname] = True
        try:
            cmnds.activate(plugname)
            callbacks.activate(plugname)
            gn_callbacks.activate(plugname)
            jcallbacks.activate(plugname)
            rebefore.activate(plugname)
            reafter.activate(plugname)
            saymonitor.activate(plugname)
            outmonitor.activate(plugname)
            xmppmonitor.activate(plugname)
            tests.activate(plugname)
            outputmorphs.activate(plugname)
            inputmorphs.activate(plugname)
        except Exception, ex: handle_exception() 

    def down(self, plugname):
        """ deactivate a plugin. """
        self.activated[plugname] = False
        try:
            cmnds.disable(plugname)
            callbacks.disable(plugname)
            gn_callbacks.disable(plugname)
            jcallbacks.disable(plugname)
            rebefore.disable(plugname)
            reafter.disable(plugname)
            saymonitor.disable(plugname)
            outmonitor.disable(plugname)
            xmppmonitor.disable(plugname)
            tests.disable(plugname)
            outputmorphs.disable(plugname)
            inputmorphs.disable(plugname)
        except Exception, ex: handle_exception()

    def unload(self, plugname):
        """ unload plugin. """
        unloaded = [plugname, ]
        try:
            plug = self.plugs[plugname]
            for p in plug.__plugs__:
                if p == plug: raise Exception("same plugin name as dir name (%s)" % plugname)
                unloaded.extend(self.unload(p))
        except (KeyError, AttributeError): pass
        for plugname in unloaded:
            self.saveplug(plugname)
            self.unloadnosave(plugname)
            try: self.avail.remove(plugname)
            except ValueError: pass
        return unloaded

    def unloadnosave(self, plugname):
        """ unload plugin without saving. """
        try:
            self.plugs[plugname].shutdown()
            rlog(10, 'plugins', '%s shutdown called' % plugname)
        except (AttributeError, KeyError): pass
        except Exception, ex: handle_exception()
        try: self.plugallow.data.remove(plugname)
        except (KeyError, ValueError): pass
        try: self.avail.remove(plugname)
        except ValueError: pass
        try: self.initcalled.remove(plugname)
        except ValueError: pass
        try:
            cmnds.unload(plugname)
            callbacks.unload(plugname)
            gn_callbacks.unload(plugname)
            jcallbacks.unload(plugname)
            rebefore.unload(plugname)
            reafter.unload(plugname)
            saymonitor.unload(plugname)
            outmonitor.unload(plugname)
            xmppmonitor.unload(plugname)
            tests.unload(plugname)
            outputmorphs.unload(plugname)
            inputmorphs.unload(plugname)
            if self.plugs.has_key(plugname): del self.plugs[plugname]
        except Exception, ex: handle_exception() ; return 0
        rlog(0, 'plugins', '%s unloaded' % plugname)
        return 1

    def whereis(self, what):
        """ locate a command. """
        return cmnds.whereis(what)

    def permoverload(self, funcname, perms):
        """ overload permission of a function. """
        if not rebefore.permoverload(funcname, perms):
            if not cmnds.permoverload(funcname, perms):
                if not reafter.permoverload(funcname, perms): return False
        return True

    def woulddispatch(self, bot, ievent):
        """ function to determine whether a event would dispatch. """
        (what, command) = self.dispatchtest(bot, ievent)
        if what and command: return True
        return False

    def dispatchtest(self, bot, ievent, direct=False):
        """ return (dispatcher, command) on which command should fire. """
        if shouldignore(ievent.userhost): return (None, None)
        if ievent.userhost in bot.throttle: return (None, None)
        if ievent.txt.find(' | ') != -1: target = ievent.txt.split(' | ')[0]
        elif ievent.txt.find(' && ') != -1: target = ievent.txt.split(' && ')[0]
        else: target = ievent.txt
        result = []
        com = rebefore.getcallback(target)
        if com and not target.startswith('!'):
            com.re = True
            result = [rebefore, com]
        else:
            if ievent.txt.startswith('!'): ievent.txt = ievent.txt[1:]
            aliascheck(ievent)
            com = cmnds.getcommand(ievent.txt)
            if com:
                com.re = False
                result = [cmnds, com]
                ievent.txt = ievent.txt.strip()
            else:
                com = reafter.getcallback(target)
                if com:
                    com.re = True
                    result = [reafter, com]
        if result:
            if config['auto_register'] and not users.getname(ievent.userhost):
                if bot.google: users.add(ievent.userhost , [ievent.userhost, ], ['USER', ])
                elif not bot.jabber:
                    users.add("%s!%s" % (ievent.nick, ievent.userhost) , [ievent.userhost, ], ['USER', ])
                    bot.ratelimit(ievent.userhost, 20)
                else:
                    if ievent.groupchat:
                        users.add(ievent.userhost , [ievent.userhost, ], ['USER', ])
                        bot.ratelimit(ievent.userhost)
                    else:
                        users.add(ievent.stripped , [ievent.stripped, ], ['USER', ])
                        bot.ratelimit(ievent.stripped, 20)
            if config['anon_enable'] and not 'OPER' in result[1].perms: return result
            if com.name in bot.state['allowed'] or getname(com.func) in bot.state['allowed']: return result
            try:
                chanperms = bot.channels[ievent.channel.lower()]['perms']
                for i in result[1].perms:
                    if i in chanperms and not ievent.msg: ievent.speed = 1 ; return result
            except (KeyError, TypeError): pass
            if direct: return result
            if bot.jabber and ievent.jabber:
                if not ievent.groupchat or ievent.jidchange:
                    if users.allowed(ievent.stripped, result[1].perms):
                        return result
            if users.allowed(ievent.userhost, result[1].perms): return result
        return (None, None)

    def cmnd(self, bot, ievent, timeout=15, response=False, onlyqueues=True):
        """ launch command and wait for result. """
        if response: ievent.reply('launching %s on %s bot' % (ievent.txt, bot.name))
        q = Queue.Queue()
        ievent.queues.append(q)
        self.trydispatch(bot, ievent)
        return waitforqueue(q, timeout)

    def waitdispatch(self, bot, ievent, direct=False):
        """ dispatch command and wait for results. """
        ievent.threaded = True
        return self.trydispatch(bot, ievent, direct, wait=True)

    def trydispatch(self, bot, ievent, direct=False, wait=False):
        """ try to dispatch ievent. """
        if shouldignore(ievent.userhost): return 0
        if ievent.msg: ievent.printto = ievent.nick
        else:  ievent.printto = ievent.channel
        (what, com) = self.dispatchtest(bot, ievent, direct)
        if what:
            if com.allowqueue:
                ievent.txt = ievent.txt.replace(' || ', ' | ')
                if ievent.txt.find(' | ') != -1:
                    if ievent.txt[0] == '!': ievent.txt = ievent.txt[1:]
                    else: self.splitpipe(bot, ievent) ; return
                elif ievent.txt.find(' && ') != -1: self.multiple(bot, ievent) ; return
            return self.dispatch(what, com, bot, ievent, wait)

    def dispatch(self, what, com, bot, ievent, wait=False):
        """ do the actual dispatch of event. """
        if bot.stopped: return False
        if com.options: makeoptions(ievent, com.options)
        else: makeoptions(ievent)
        makeargrest(ievent)
        ievent.usercmnd = True
        rlog(10, 'plugins', 'dispatching %s for %s' % (ievent.command, ievent.userhost))
        what.dispatch(com, bot, ievent, wait)
        return True

    def clonedevent(self, bot, event):
        """ clone an event. """
        ie = copy.deepcopy(event)
        return ie

    def multiple(self, bot, ievent):
        """ execute multiple commands. """
        for i in ievent.txt.split(' && '):
            ie = self.clonedevent(bot, ievent)
            ie.txt = i
            #self.needreloadcheck(bot, ievent)
            self.trydispatch(bot, ie)

    def splitpipe(self, bot, ievent):
        """ execute commands in a pipeline. """
        origqueues = ievent.queues
        ievent.queues = []
        events = []
        txt = ievent.txt.replace(' || ', ' ##')
        # make the events
        for i in txt.split(' | '):
            item = i.replace(' ##', ' | ')
            ie = self.clonedevent(bot, ievent)
            ie.userhost = ievent.userhost
            ie.onlyqueues = True
            ie.txt = item.strip()
            #self.needreloadcheck(bot, ie)
            events.append(ie)
        prevq = None
        for i in events[:-1]:
            q = Queue.Queue()
            i.queues.append(q)
            if prevq: i.inqueue = prevq
            prevq = q
        events[-1].inqueue = prevq
        events[-1].onlyqueues = False
        if origqueues: events[-1].queues = origqueues
        # check if all commands would dispatch
        for i in events:
            if not self.woulddispatch(bot, i):
                ievent.reply("can't execute %s" % str(i.txt))
                return
        # do the dispatch
        for i in events:
            (what, com) = self.dispatchtest(bot, i)
            if what:
                self.dispatch(what, com, bot, i)

    def needreloadcheck(self, bot, event, target=None):
        """ check if a plugin needs to be reloaded. """
        if cmndtable:
            try: 
                if target:
                    rlog(10, 'plugins', 'target set: %s' % target)
                    cmnd = 'target-set'
                    plugin = target
                else:
                    t = event.txt.split()[0]
                    cmnd = aliasget(t) or t
                    plugin = cmndtable[cmnd]
                rlog(10, 'plugins', 'cmnd: %s plugin: %s' % (cmnd, plugin))
                if self.exist(plugin): return
                try: self.reload('gozerbot.plugs', plugin)
                except ImportError, ex:
                    try: self.reload('gplugs', plugin)
                    except ImportError, ex:
                        if config.get('db_driver') == 'olddb':
                            try: self.reload('gplugs.olddb', plugin)
                            except ImportError, ex: pass
                        else:
                            try: self.reload('gplugs.alchemy', plugin)
                            except ImportError, ex: pass
                        try: self.reload('myplugs', plugin)
                        except ImportError, ex: return
                rlog(100, 'plugins', 'reloaded %s' % plugin)
            except KeyError, ex: rlog(10, 'plugins', "can't find plugin to reload for %s" % event.txt.split()[0])

    def listreload(self, pluglist):
        """ reload list of plugins. """
        failed = []
        for what in pluglist:
            splitted = what[:-3].split(os.sep)
            mod = '.'.join(splitted[:-1])
            if not mod:
                if config.get('db_driver') == "olddb": mod = "gplugs.olddb"
                elif config.get("db_driver") == "alchemy": mod = "gplugs.alchemy"
                else: mod = 'gplugs'
            plug  = splitted[-1]
            try: self.reload(mod, plug)
            except Exception, ex: failed.append(what)
        return failed

## defines

plugins = Plugins()
